#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import json
import os
import re
import stat
import sys

from common.cleaner import clear
from common.common import execute_cmd, check_command_injection
from common.const import AuthType, SysData
from sqlserver import log
from sqlserver.commons.common import get_key_value, check_del_dir_or_file, output_tmp_info, exec_pw_shell_filename
from sqlserver.commons.const import BodyErr, SQLServerCode, TmpFile, CodeIdConst
from sqlserver.commons.const import ParamConstant, JobData, SqlCmd, SqlSystemId, SQLServerStrConstant, ExecCmdResult
from sqlserver.commons.sqlserver_parse_param import VDIAPINAME
from sqlserver.commons.sqlserver_utils import SqlServerUtils


class SqlServerResource:
    m_func_type = ""
    m_instance_name = ""
    m_network_name = ""
    m_authentication_type = 0
    m_check_output = {'code': SQLServerCode.FAILED.value, 'bodyErr': 0, 'message': ''}
    m_resource_output = {'resourceList': []}
    m_username = ""
    m_password = ""
    sql_utils = None
    user_info = ""
    pass_prefix = ""

    @staticmethod
    def read_tmp_result_file():
        file_path = TmpFile.OUT_FILE
        with open(file_path, "r", encoding='UTF-8') as f:
            ret = f.read()
        check_del_dir_or_file(file_path)
        ret = ret.encode('utf-8').decode("utf-8-sig")
        return ret.strip('\n')

    @staticmethod
    def check_result_path():
        """
        check result path exit or not
        """
        log.info("check result path exit or not")
        if os.path.exists(ParamConstant.RESULT_PATH):
            return True
        else:
            log.error("The result file path does not exist!")
            return False

    @staticmethod
    def write_ag_sql_list():
        sqlcmd = "select T3.name, T2.name, " \
                 "T2.group_id  from sys.dm_hadr_database_replica_states T1 " \
                 "left JOIN sys.availability_groups T2 on T1.group_id = T2.group_id " \
                 "left JOIN master.sys.databases T3 on T1.database_id = T3.database_id;\ngo\nexit()\ngo\n"
        tmp_file = TmpFile.AG_LIST_FILE
        flags = os.O_WRONLY | os.O_CREAT
        modes = stat.S_IWUSR | stat.S_IRUSR
        with os.fdopen(os.open(tmp_file, flags, modes), 'w+') as out_file:
            out_file.write(sqlcmd)

    @staticmethod
    def write_result_to_file(in_str):
        log.info("Enter write_result_to_file")
        file_name = "{}{}".format("result", JobData.PID)
        file_path = os.path.join(ParamConstant.RESULT_PATH, file_name)
        log.info(f'{file_path}')
        output_tmp_info(file_path, in_str)
        log.info("Leave write_result_to_file")

    @staticmethod
    def write_to_ps_script(cmd, file_name):
        tmp_script = os.path.join(ParamConstant.PARAM_FILE_PATH, file_name)
        flags = os.O_WRONLY | os.O_CREAT
        modes = stat.S_IWUSR | stat.S_IRUSR
        with os.fdopen(os.open(tmp_script, flags, modes), 'w+') as out_file:
            out_file.write(cmd)

    @staticmethod
    def judgments_is_cluster():
        log.debug("Start to check if is cluster")
        flags, std_out, std_err = exec_pw_shell_filename(SqlCmd.CLUSTER_INFO)
        if flags:
            return True
        if len(std_out) != 0:
            return True
        return False

    @staticmethod
    def check_pw_shell_exec_policy():
        flags, std_out, std_err = exec_pw_shell_filename(SqlCmd.EXECUTION_POLICY)
        log.debug(f"Pw shell exec policy: {std_out}")
        if not flags:
            return True
        return False

    def check_database_list(self):
        """
        检查数据库列表是否包含特殊字符
        :return:
        """
        database_list = self.sql_utils.get_instance_databases()
        for dt_name in database_list:
            if not re.match('^[0-9a-zA-Z\u4e00-\u9fa5\u00c0-\u00ff _$-.]+$', dt_name):
                log.error(f"Database: {dt_name} have special characters!")
                return False
        return True

    def set_exception_info(self, code, msg):
        self.m_check_output['code'] = code
        self.m_check_output['message'] = msg
        self.m_check_output['bodyErr'] = code
        self.write_result_to_file(json.dumps(self.m_check_output))

    def get_ag_name_list(self):
        log.info("Enter get_ag_name_list....")
        SqlServerResource.write_ag_sql_list()
        cmds = f'sqlcmd {self.user_info} -i "{TmpFile.AG_LIST_FILE}" -o "{TmpFile.OUT_FILE}" -W ' \
               f'-f {CodeIdConst.WINDOWS_CODE}'
        ret, std_out, std_err = self.sql_utils.get_command_result(cmds)
        check_del_dir_or_file(TmpFile.AG_LIST_FILE)
        if ret != "0":
            return False, std_out
        out_str = self.read_tmp_result_file()
        idxs = out_str.find("Error")
        if int(idxs) != -1:
            return False, out_str
        resource_list_value = self.m_resource_output.get('resourceList', "Not exist")
        if resource_list_value == "Not exist":
            return False, "json format error"
        if len(out_str.split("\n")) <= 2:
            return True, ""
        json_str = out_str.split("\n")[0:-2]
        log.info(f"{json_str}")
        for json_member in json_str:
            # 内容格式：cs_test cs_ag 995B6408-BA9A-4E7E-9347-290720F29166
            dt_name = json_member.split(" ")[0]
            for res_member in resource_list_value:
                dt_name_tmp = res_member.get('name', "")
                if dt_name_tmp == dt_name:
                    extend_infos = res_member.get('extendInfo', {})
                    extend_infos['agName'] = json_member.split(" ")[1]
                    extend_infos['agId'] = json_member.split(" ")[2]
                    break
        log.info("Leave get_ag_name_list....")
        return True, ""

    def get_param_from_input(self, pid):
        file_name = "{}{}".format("param", pid)
        file_path = os.path.join(ParamConstant.PARAM_FILE_PATH, file_name)
        if not os.path.exists(file_path):
            raise Exception(f"File:{file_path} not exist from SqlServerResource.get_param_from_input")
        flags, self.m_authentication_type = get_key_value(f'application_auth_authType_{JobData.PID}')
        with open(file_path, 'r') as f:
            data = json.load(f)
            self.m_instance_name = data.get('application', {}).get('name', "Not exist")
            self.m_network_name = data.get('application', {})\
                .get('extendInfo', {}).get('networkName', "")
        check_del_dir_or_file(file_path)
        if check_command_injection(self.m_instance_name):
            log.error(f"The param: %s has special characters.", self.m_instance_name)
            raise Exception("The param has special characters.")
        if check_command_injection(self.m_network_name):
            log.error(f"The param: %s has special characters.", self.m_network_name)
            raise Exception("The param has special characters.")
        if self.m_network_name == "":
            self.m_network_name = SqlServerUtils.get_hostname()
        if self.m_instance_name and \
                self.m_instance_name.upper() != SQLServerStrConstant.SQLSERVER_DEFAULT_SERVICES.upper():
            instance = f'\"{self.m_network_name}\\\\{self.m_instance_name}\"'
        else:
            instance = self.m_network_name
        if int(self.m_authentication_type) == AuthType.APP_PASSWORD.value:
            self.get_userinfo_from_env_var()
            self.user_info = f"-S {instance} -U {self.m_username}"
        elif int(self.m_authentication_type) == AuthType.OS_PASSWORD.value:
            self.user_info = f"-S {instance}"
        else:
            log.error(f"Auth mode: {self.m_authentication_type} is not support")
            raise Exception("Auth mode not support")
        self.sql_utils = SqlServerUtils(self.m_authentication_type, self.user_info, self.pass_prefix, pid)

    def get_userinfo_from_env_var(self):
        flags, self.m_username = get_key_value(f'application_auth_authKey_{JobData.PID}')
        if not flags:
            log.error(f"get user name failed")
            return
        flags, self.m_password = get_key_value(f"application_auth_authPwd_{JobData.PID}")
        if not flags:
            log.error(f"get user password failed")
            return
        if not self.m_username or not self.m_password:
            log.error("get user info failed")
        if check_command_injection(self.m_username):
            log.error(f"The param: %s has special characters.", self.m_username)
            raise Exception("The param has special characters.")
        self.pass_prefix = f"application_auth_authPwd_{JobData.PID}"

    def get_winows_input(self, input_func):
        """
        get input from system command line
        """
        log.info("Get input from system command line")
        self.m_func_type = input_func

    def check_authentication(self):
        log.info("check Authentication")
        cmd = f'sqlcmd {self.user_info} -Q "exit;"'
        ret, std_out, std_err = self.sql_utils.get_command_result(cmd)
        log.info(f"ret:{ret},std_out:{std_out},std_err:{std_err}")
        if ret != ExecCmdResult.SUCCESS:
            log.error(f'Auth failed, Err code: {ret}')
            raise Exception("Auth failed")

    def find_sql_list_from_master(self):
        log.info("Find sql list")
        sql_cmd = "SELECT name, database_id, compatibility_level, state FROM master.sys.databases;"
        cmd = f'sqlcmd {self.user_info} -Q "{sql_cmd}" -s "," -o "{TmpFile.OUT_FILE}" -W -f 65001'
        ret, _, _ = self.sql_utils.get_command_result(cmd)
        if ret != ExecCmdResult.SUCCESS:
            return False, ""
        out_str = self.read_tmp_result_file()
        idxs = out_str.find("Error")
        if int(idxs) != -1:
            return False, out_str
        return True, out_str

    def write_resource_to_json(self, in_str, versions):
        js_str = in_str.split('\n')[2:-2]
        # 非默认实例ReportServer和ReportServerTempDB名称不一样，且不显示在数据库列表
        if self.m_instance_name.upper() == SQLServerStrConstant.SQLSERVER_DEFAULT_SERVICES.upper():
            report_list = ("ReportServer", "ReportServerTempDB")
        else:
            report_list = (f"ReportServer${self.m_instance_name}", f"ReportServer${self.m_instance_name}TempDB")
        for info in js_str:
            database_info = info.split(",")
            database_id_value = database_info[1]
            if int(database_id_value) < SqlSystemId.MIN_ID:
                continue
            state_value = database_info[3]
            name_value = database_info[0]
            if name_value in report_list:
                continue
            node_exend = {
                'status': state_value, 'version': versions,
                'networkName': self.m_network_name, 'databaseId': database_id_value,
                'hostName': self.m_network_name,
                'instanceName': self.m_instance_name,
                'agName': '',
                'agId': ''
            }
            node_info = {
                'endpoint': '',
                'extendInfo': node_exend,
                'name': name_value,
                'role': '',
                'subType': 'SQLServer-database',
                'type': 'Database',
                'uuid': ''
            }
            resource_list_value = self.m_resource_output.get('resourceList', "Not exist")
            resource_list_value.append(node_info)

    def list_appliction_resource(self):
        log.info("Enter list_appliction_resource")
        try:
            self.get_param_from_input(JobData.PID)
        except Exception as e:
            self.set_exception_info(SQLServerCode.FAILED.value, str(e))
            log.error("ListApplictionResource error")
            return False
        version_str = self.sql_utils.get_version()
        flags, str_tmp = self.find_sql_list_from_master()
        if not flags:
            self.set_exception_info(SQLServerCode.FAILED.value, "")
            log.error("ListApplictionResource error")
            return False
        try:
            self.write_resource_to_json(str_tmp, version_str)
        except Exception as e:
            self.set_exception_info(SQLServerCode.FAILED.value, str(e))
            log.error("ListApplictionResource error")
            return False
        flags, msgs = self.get_ag_name_list()
        if not flags:
            log.error('Get ag group failed')
            return False
        self.write_result_to_file(json.dumps(self.m_resource_output))
        log.info("Leave list_appliction_resource")
        return True

    def authentication(self):
        try:
            self.get_param_from_input(JobData.PID)
        except Exception as e:
            self.set_exception_info(SQLServerCode.FAILED.value, str(e))
            log.error("Authentication error")
            return False
        try:
            self.check_authentication()
        except Exception as e:
            log.exception(f"Failed check auth")
            self.set_exception_info(BodyErr.SQLSERVER_AUTH_INFO_ERROR.value, str(e))
            return False
        flags = self.check_database_list()
        if not flags:
            self.set_exception_info(BodyErr.SQLSERVER_DATABASE_NAME_INVALID.value, "Invalid characters.")
            return False
        version = self.sql_utils.get_version()
        check_dict = {"version": version}
        self.m_check_output['code'] = 0
        self.m_check_output['message'] = json.dumps(check_dict)
        self.write_result_to_file(json.dumps(self.m_check_output))
        log.info("Authentication Success")
        return True

    def real_auth(self):
        if SqlServerResource.check_pw_shell_exec_policy():
            log.error("Powershell execution permission denied.")
            self.m_check_output['code'] = SQLServerCode.FAILED.value
            self.m_check_output['bodyErr'] = BodyErr.NO_WINDOWS_POWER_SHELL_PERMISSION.value
            self.m_check_output['message'] = "Powershell execution permission denied."
            self.write_result_to_file(json.dumps(self.m_check_output))
            return False
        if SqlServerResource.judgments_is_cluster():
            log.error("This node belongs to a cluster.")
            self.m_check_output['code'] = SQLServerCode.FAILED.value
            self.m_check_output['bodyErr'] = BodyErr.SQLSERVER_REGISTER_TYPE_ERROR.value
            self.m_check_output['message'] = "This node belongs to a cluster."
            self.write_result_to_file(json.dumps(self.m_check_output))
            return False
        self.authentication()
        return True

    def clear_pwd(self):
        clear(self.m_password)


if __name__ == '__main__':
    log.info("Running main....")
    for line in sys.stdin:
        SysData.SYS_STDIN = line
        break
    newInstance = SqlServerResource()
    if not newInstance.check_result_path():
        sys.exit(1)
    newInstance.get_winows_input(sys.argv[1])
    JobData.PID = sys.argv[2]
    if newInstance.m_func_type == "CheckApplication":
        newInstance.real_auth()
        newInstance.clear_pwd()
        clear(SysData.SYS_STDIN)
    elif newInstance.m_func_type == "ListApplicationResource":
        newInstance.list_appliction_resource()
        newInstance.clear_pwd()
        clear(SysData.SYS_STDIN)
    else:
        log.error("Wrong input from command line!")
